package container

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	"gitee.com/chuanjinge/go-container/container/cgroup"
	"gitee.com/chuanjinge/go-container/container/containerconfig"
	"gitee.com/chuanjinge/go-container/container/info"
	"gitee.com/chuanjinge/go-container/container/vethbr"
	"gitee.com/chuanjinge/go-container/option"
	"gitee.com/chuanjinge/go-container/pkg"
	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netns"
)

// 开始clone namespace
// 创建的同时绑定一对管道
//
// @param tty bool 是否开启交互窗口
// @param containerId string 预分配的容器ID
// @param *option.CmdOption option 命令行标识符
// @return exec.Cmd
// @return os.File
func CreateNamespace(containerId string, option *option.CmdOption) (*exec.Cmd, *os.File) {
	// 和C代码一样，此时我们应该启动一个子进程，并设置指定的namespace
	// 这里好像是重新启动了自己，不过尾随的参数改为了 init
	exePath, err := os.Executable()
	if err != nil {
		log.Fatal(err)
		return nil, nil
	}
	// 将run命令改为init命令，我们要重定向一下，免得继续执行run造成产生循环重定向
	// os/exec.Command 会fork一个子进程执行指定的命令。
	containerIdFlag := fmt.Sprintf("--containerId=%s", containerId)
	args := []string{"init", containerIdFlag}
	for _, volume := range option.Volume {
		volumeFlag := fmt.Sprintf("--volume=%s", volume)
		args = append(args, volumeFlag)
	}
	log.Printf("参数列表: %v", args)
	cmd := exec.Command(exePath, args...)

	// 让新的进程的命名空间发生变化，脱离当前父进程的命名空间
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
	}
	cmd.Env = append(os.Environ(), option.Env...)

	// 创建一个 pipe 管道，稍后 exec.Command 创建子进程的时候带入到子进程之中
	writePipe, err := CreatePipeToCmd(cmd)
	if err != nil {
		log.Fatal(err)
		return nil, nil
	}

	return cmd, writePipe
}

// 以新的命名空间+子进程的方式运行接收到的参数
// @param bool tty 是否是交互方式
// @param []string args 字符串数组格式
// @param *option.CmdOption option 命令行的标识符
func RunWithNewNamespace(containerId string, args []string, option *option.CmdOption) *info.ContainerInfo {
	// 将容器初始化信息写入配置文件
	cInfo := logContainerInfo(containerId, option)

	var err error
	newProc, writePipe := CreateNamespace(containerId, option)
	if option.TTY {
		// 如果用户声明了使用伪终端运行子进程，那我们就得使用伪终端，哪怕他指明了是守护进程
		// 伪终端会包含作业控制、信号捕捉等功能，在守护进程模式下做了特殊处理，以避免类似 xxx run -itd -- /bin/bash 这种情况直接退出，因为理论上声明了交互式就可以通过伪终端读取输入给 /bin/bash, 他应该阻塞在后台进程才对。
		err = runContainerWithPty(containerId, newProc, option, writePipe, args, cInfo)
	} else {
		err = runContainerWithoutPty(containerId, newProc, option, writePipe, args, cInfo)
	}

	if err != nil {
		cInfo.State.Running = false
		cInfo.State.Paused = true
		log.Print(err)
	}

	// 等待结束信号
	_ = newProc.Wait()
	return cInfo
}

func logContainerInfo(containerId string, option *option.CmdOption) *info.ContainerInfo {
	mountPointList := make(map[string]*info.ContainerMountPoint, 0)
	for _, volume := range option.Volume {
		pathList := strings.Split(volume, ":")
		hostPath := pathList[0]
		containerPath := pathList[1]

		if hostPath == "" || containerPath == "" {
			continue
		}
		mountPoint := new(info.ContainerMountPoint)
		mountPoint.Source = hostPath
		mountPoint.Destination = containerPath
		mountPoint.Type = "bind"
		mountPointList[hostPath] = mountPoint
	}
	cInfo := &info.ContainerInfo{
		ID:    containerId,
		Name:  containerId,
		State: info.ContainerInfoState{},
		Config: info.ContainerInfoConfig{
			Hostname: containerId,
		},
		LogPath:     containerconfig.GetConfigLog(containerId),
		Created:     time.Now(),
		MountPoints: mountPointList,
		NetworkSettings: info.ContainerInfoNetwork{
			Bridge:   "unknown",
			Networks: make(map[string]info.ContainerInfoNetworkItem),
			Ports:    make(map[int16]int16, 0),
		},
	}
	pkg.LogConf(cInfo)
	return cInfo
}

// 没有伪终端的情况下运行容器
func runContainerWithoutPty(containerId string, newProc *exec.Cmd, option *option.CmdOption, writePipe *os.File, args []string, cInfo *info.ContainerInfo) error {
	if err := newProc.Start(); err != nil {
		return err
	}
	log.Printf("子进程ID: %d\n", newProc.Process.Pid)
	return setContainerEnvAfterRun(containerId, newProc, option, writePipe, args, cInfo)
}

// 容器进程运行之后的一些环境设置
func setContainerEnvAfterRun(containerId string, newProc *exec.Cmd, option *option.CmdOption, writePipe *os.File, args []string, cInfo *info.ContainerInfo) error {
	cInfo.State.Pid = newProc.Process.Pid
	cInfo.State.Running = true
	cInfo.State.StartedAt = time.Now()
	cInfo.Command = fmt.Sprintf("%v", args)
	pkg.LogConf(cInfo)
	file, _ := os.Stat(cInfo.LogPath)
	log.Printf("here file size: %d\n", file.Size())

	// 子进程启动成功后，针对子进程创建cgroup资源限制
	if option.MemoryLimit > 0 { // 如果命令行设置了内存限制，则此处做内存的限制
		log.Printf("内存限制: %d 字节\n", option.MemoryLimit)
		if err := setMemoryLimit(newProc, containerId, option.MemoryLimit); err != nil {
			return err
		}
	}

	// 添加veth-pairs，让容器内外可以通信
	if err := setContainerNetwork(newProc, containerId, option, cInfo); err != nil {
		log.Printf("网络设置失败: %v\n", err)
		fmt.Printf("网络设置失败: %v\n", err)
	}

	pkg.LogConf(cInfo)

	// 不能在这里执行那一系列的挂载，因为这里是父进程的进程空间，根本就没有各种命名空间，如果在这里挂载就相当于在宿主机的默认命名空间挂载了
	// 子进程启动成功之后，通过管道的write端，写入客户端非标识符参数到子进程之中
	log.Println("命令行命令和参数:", args)
	PushMsgToPipeAndClose(writePipe, args)
	return nil
}

func setMemoryLimit(newProc *exec.Cmd, hierarchy string, limit int) error {
	cgroupPath, err := cgroup.JoinHierarchy("memory", hierarchy)
	if err != nil {
		return err
	}
	err = cgroup.SetLimit(cgroupPath, "memory.limit_in_bytes", strconv.Itoa(limit))
	if err != nil {
		return err
	}
	err = cgroup.AppendPid(cgroupPath, newProc.Process.Pid)
	if err != nil {
		return err
	}

	return nil
}

// 设置网络的虚拟以太网对
func setContainerNetwork(newProc *exec.Cmd, containerId string, option *option.CmdOption, cInfo *info.ContainerInfo) error {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	// [在宿主机中]先保留初始状态的netns,待会NewNamed之后切回这个初始netns
	oldNetNs, err := netns.Get()
	if err != nil {
		log.Printf("[在宿主机]获取原始netns失败: %v\n", err)
		return err
	}
	defer oldNetNs.Close()

	endpoint, err := vethbr.NewEndpoint(containerId)
	if err != nil {
		return err
	}
	log.Printf("[SetContainerNetwork]将容器端:%s放入容器中", endpoint.Device.LinkAttrs.Name)
	// 把一端放入容器的空间中
	linkMaster, err := netlink.LinkByName(endpoint.Device.LinkAttrs.Name)
	if err != nil {
		log.Printf("[在宿主机]netlink.LinkByName 获取linkMaster失败了: %v\n", err)
		return err
	}
	netNsPath := fmt.Sprintf("/proc/%d/ns/net", newProc.Process.Pid)
	nsHandle, err := netns.GetFromPath(netNsPath)
	if err != nil {
		log.Printf("[在宿主机]netns.LinGetFromPath获取namespace路径%s失败了: %v\n", netNsPath, err)
		return err
	}
	if err := netlink.LinkSetNsFd(linkMaster, int(nsHandle)); err != nil {
		log.Printf("[在宿主机中]把主端veth-pair移动到的新的匿名空间中失败:%v\n", err)
		return err
	}

	// 如果设置了网络，则将peer端插入网桥之中
	if option.Network != nil {
		log.Printf("将宿主机端插入到网桥中...")
		br := &vethbr.BridgeDriver{}
		if err := br.Connect(option.Network, endpoint); err != nil {
			log.Printf("[在宿主机中]把对端veth-pair插入网桥失败:%v\n", err)
			return err
		}
	}

	// !+[container net namespace]
	// 切换进入匿名的 netns 中
	err = netns.Set(nsHandle)
	if err != nil {
		log.Printf("Failed to switch to new namespace: %v\n", err)
		return err
	}
	// 在匿名空间中，重新获取网络接口的文件描述符，因为可能填进来之后发生了变化
	linkMasterInNs, err := netlink.LinkByName(endpoint.Device.LinkAttrs.Name)
	if err != nil {
		log.Printf("[在匿名空间]获取接口偶数端的文件描述符失败: %v", err)
		netns.Set(oldNetNs)
		return err
	}
	if err := netlink.LinkSetUp(linkMasterInNs); err != nil {
		log.Printf("[在匿名空间]激活偶数端失败: %v", err)
		netns.Set(oldNetNs)
		return err
	}
	// 把回环接口也启动，以便可以自己访问自己
	if linkLO, err := netlink.LinkByName("lo"); err != nil {
		netlink.LinkSetUp(linkLO)
	}

	var containerIPNet *net.IPNet

	// 如果设置了网桥的话，基于网桥的子网段给当前网卡分配ip地址，以便可以通过该网桥进行通信
	if option.Network != nil {
		ipHandler := vethbr.NewSimpleIPHandler()
		ipAlloc, err := ipHandler.Allocate(option.Network.IPRange)
		log.Printf("可用IP地址: %s\n", ipAlloc.To4().String())
		if err != nil {
			log.Printf("[在匿名空间]分配IP地址失败: %v", err)
			netns.Set(oldNetNs)
			return err
		}
		ipNetAlloc := &net.IPNet{
			IP:   ipAlloc.To4(),
			Mask: option.Network.IPRange.Mask,
		}
		addr := &netlink.Addr{IPNet: ipNetAlloc}
		if err := netlink.AddrAdd(linkMasterInNs, addr); err != nil {
			log.Printf("[在匿名空间]设置IP地址失败: %v", err)
			netns.Set(oldNetNs)
			ipHandler.Release(option.Network.IPRange, ipAlloc)
			return err
		}
		containerIPNet = ipNetAlloc

		// 将当前网络空间的默认路由设置为网桥...
		// 所有请求都设置为网桥
		_, cidr, _ := net.ParseCIDR("0.0.0.0/0")
		// route add -net 0.0.0.0/0 <GATEWAY-IP> <DEVICE>
		defaultRoute := &netlink.Route{
			LinkIndex: linkMasterInNs.Attrs().Index,
			Gw:        option.Network.GatewayIP,
			Dst:       cidr,
		}
		log.Printf("网关ip地址: %s\n", option.Network.GatewayIP.String())

		if err := netlink.RouteAdd(defaultRoute); err != nil {
			netns.Set(oldNetNs)
			ipHandler.Release(option.Network.IPRange, ipAlloc)
			return err
		}

	}

	// 切换回默认空间
	err = netns.Set(oldNetNs)
	if err != nil {
		log.Println("[NETNS-SET]error:", err.Error())
	}
	/*
		tmpCmd := exec.Command("ip", "link")
		tmpOutput, err := tmpCmd.Output()
		if err != nil {
			log.Println("[IP-LINK]error:", err.Error())
		}
		log.Println(string(tmpOutput))
	*/
	// !-[container net namespace]

	// 设置端口映射
	// 前提是设置了端口、指定了网络、分配好了IP
	if option.Port != nil && option.Network != nil && containerIPNet != nil {
		log.Printf("设置端口映射: %#v\n", option.Port)
		for hostPort, containerPort := range option.Port {
			log.Printf("设置端口映射: %d:%d\n", hostPort, containerPort)
			if err := vethbr.SetupIPTablesDNAT(option.Network.Name, hostPort, containerIPNet.IP, containerPort); err != nil {
				log.Printf("[ERROR]设置DNAT规则失败:%s %d %s %d; err: %v\n", option.Network.Name, hostPort, containerIPNet.IP.String(), containerPort, err)
			} else {
				// 写入配置文件
				cInfo.NetworkSettings.Ports[containerPort] = hostPort
			}
		}

		networkItem := info.ContainerInfoNetworkItem{
			Gateway:   option.Network.GatewayIP.String(),
			IPAddress: containerIPNet.IP.String(),
		}
		cInfo.NetworkSettings.Networks[option.Network.Name] = networkItem
		log.Printf("[NetworkSettings]name:%s;info: %#v\n", option.Network.Name, networkItem)
	}
	return nil
}
